package me.ydq.core;


import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import me.ydq.convert.ColumnConvert;
import me.ydq.core.sql.Sql;
import me.ydq.misc.Fn;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

@FieldDefaults(makeFinal = true, level = AccessLevel.PACKAGE)
@Getter(AccessLevel.PACKAGE)
final class JdbcSqlSpec<T> implements SqlSpec<T> {
    Class<T>             cls;
    AtomicInteger        index;
    List<SqlCriteria<T>> criteria = new ArrayList<>();
    Map<String, Object>  params;

    public JdbcSqlSpec(Class<T> cls) {
        this.cls = cls;
        this.index = new AtomicInteger();
        this.params = new HashMap<>();
    }

    /**
     * 供 OR / HAVING 等独立场景使用，复用外层的 JdbcSqlSpec 里面的 params 和 index
     * 便于将参数和占位符参数索引全部放在 where 的params里面
     */
    public JdbcSqlSpec(JdbcSqlSpec<T> reuse) {
        this.cls = reuse.cls;
        this.index = reuse.index;
        this.params = reuse.params;
    }

    String paramsName(Fn<T, ?> fn) {
        return fn.toUnderLine() + "_" + index.incrementAndGet();
    }

    String paramsName() {
        return "param_" + index.incrementAndGet();
    }

    //region is null
    public SqlSpec<T> isNull(Fn<T, ?> fn) {
        criteria.add(SqlCriteria.of(cls, Sql::IS_NULL, fn));
        return this;
    }

    public SqlSpec<T> isNull(String rawColumn) {
        criteria.add(SqlCriteria.of(cls, Sql::IS_NULL, rawColumn));
        return this;
    }
    //endregion

    //region is not null
    public SqlSpec<T> isNotNull(Fn<T, ?> fn) {
        criteria.add(SqlCriteria.of(cls, Sql::IS_NOT_NULL, fn));
        return this;
    }

    public SqlSpec<T> isNotNull(String rawColumn) {
        criteria.add(SqlCriteria.of(cls, Sql::IS_NOT_NULL, rawColumn));
        return this;
    }
    //endregion

    //region eq
    public SqlSpec<T> eq(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::EQ, fn, paramsName));
        return this;
    }

    public SqlSpec<T> eq(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::EQ, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region not eq
    public SqlSpec<T> ne(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_EQ, fn, paramsName));
        return this;
    }

    public SqlSpec<T> ne(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_EQ, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region gt
    public SqlSpec<T> gt(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::GT, fn, paramsName));
        return this;
    }

    public SqlSpec<T> gt(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::GT, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region ge
    public SqlSpec<T> ge(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::GE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> ge(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::GE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region lt
    public SqlSpec<T> lt(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LT, fn, paramsName));
        return this;
    }

    public SqlSpec<T> lt(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LT, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region le
    public SqlSpec<T> le(Fn<T, ?> fn, Object value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> le(String rawColumn, Object value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region in
    public SqlSpec<T> in(Fn<T, ?> fn, Collection<?> value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::IN, fn, paramsName));
        return this;
    }

    public SqlSpec<T> in(String rawColumn, Collection<?> value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::IN, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region not in
    public SqlSpec<T> notIn(Fn<T, ?> fn, Collection<?> value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_IN, fn, paramsName));
        return this;
    }

    public SqlSpec<T> notIn(String rawColumn, Collection<?> value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_IN, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region between
    public SqlSpec<T> between(Fn<T, ?> fn, Object from, Object to) {
        String fromName = paramsName(fn);
        String toName   = paramsName(fn);
        params.put(fromName, from);
        params.put(toName, to);
        criteria.add(SqlCriteria.of(cls, Sql::BETWEEN, fn, List.of(fromName, toName)));
        return this;
    }

    public SqlSpec<T> between(String rawColumn, Object from, Object to) {
        String fromName = paramsName();
        String toName   = paramsName();
        params.put(fromName, from);
        params.put(toName, to);
        criteria.add(SqlCriteria.of(cls, Sql::BETWEEN, rawColumn, List.of(fromName, toName)));
        return this;
    }
    //endregion

    //region not between
    public SqlSpec<T> notBetween(Fn<T, ?> fn, Object from, Object to) {
        String fromName = paramsName(fn);
        String toName   = paramsName(fn);
        params.put(fromName, from);
        params.put(toName, to);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_BETWEEN, fn, List.of(fromName, toName)));
        return this;
    }

    public SqlSpec<T> notBetween(String rawColumn, Object from, Object to) {
        String fromName = paramsName();
        String toName   = paramsName();
        params.put(fromName, from);
        params.put(toName, to);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_BETWEEN, rawColumn, List.of(fromName, toName)));
        return this;
    }
    //endregion

    //region contains
    public SqlSpec<T> contains(Fn<T, ?> fn, String value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, "%" + value + "%");
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> contains(String rawColumn, String value) {
        String paramsName = paramsName();
        params.put(paramsName, "%" + value + "%");
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region startsWith
    public SqlSpec<T> startsWith(Fn<T, ?> fn, String value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value + "%");
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> startsWith(String rawColumn, String value) {
        String paramsName = paramsName();
        params.put(paramsName, value + "%");
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region endsWith
    public SqlSpec<T> endsWith(Fn<T, ?> fn, String value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, "%" + value);
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> endsWith(String rawColumn, String value) {
        String paramsName = paramsName();
        params.put(paramsName, "%" + value);
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region like
    public SqlSpec<T> like(Fn<T, ?> fn, String value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> like(String rawColumn, String value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::LIKE, rawColumn, paramsName));
        return this;
    }
    //endregion

    //region not like
    public SqlSpec<T> notLike(Fn<T, ?> fn, String value) {
        String paramsName = paramsName(fn);
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_LIKE, fn, paramsName));
        return this;
    }

    public SqlSpec<T> notLike(String rawColumn, String value) {
        String paramsName = paramsName();
        params.put(paramsName, value);
        criteria.add(SqlCriteria.of(cls, Sql::NOT_LIKE, rawColumn, paramsName));
        return this;
    }

    //endregion

    //region or
    public SqlSpec<T> or(List<Consumer<SqlSpec<T>>> builders) {
        if (!builders.isEmpty()) {
            List<Object> childCriteria = new ArrayList<>();
            builders.stream()
                    .map(b -> {
                        JdbcSqlSpec<T> build = new JdbcSqlSpec<>(this);
                        b.accept(build);
                        return build.criteria;
                    })
                    .filter(c -> !c.isEmpty())
                    .forEach(childCriteria::add);
            this.criteria.add(SqlCriteria.of(cls, Sql::OR, childCriteria));
        }
        return this;
    }
    //endregion


    public String updateSets(ColumnConvert convert, Map<Fn<T, ?>, ?> sets) {
        List<String> setSql = new ArrayList<>();
        sets.forEach((fn, val) -> {
            String paramsName = paramsName(fn);
            params.put(paramsName, val);
            setSql.add(convert.convert(cls, fn) + " = :" + paramsName);
        });
        return String.join(" , ", setSql);
    }
}
